// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Xml;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
using System.IO;


using Microsoft.Build.Framework;
using Microsoft.Build.BuildEngine.Shared;

namespace Microsoft.Build.BuildEngine
{
    /// <summary>
    /// This class represents a single item of the project. An item is usually a file on disk, with a type associated with it, and
    /// its own item-specific attributes. The list of items is initially specified via XML tags in the project file, although a
    /// single item tag can represent multiple items through the use of standard wilcards * and ?. Also, tasks can add new items
    /// of various types to the project's item list -- these items don't have any XML representation.
    /// </summary>
    /// <owner>RGoel</owner>
    [DebuggerDisplay("BuildItem (Name = { Name }, Include = { Include }, FinalItemSpec = { FinalItemSpec }, Condition = { Condition } )")]
    public class BuildItem
    {
        #region Member Data

        // Whether the item is part of the project "manifest" - ie, in the project outside of a target.
        // Note that items specified inside of targets may have backing XML, but aren't considered persisted.
        private bool isPartOfProjectManifest;

        // Object holding the backing Xml, if any
        // For virtual items (i.e., those generated by tasks), this is null.
        private BuildItemGroupChildXml xml;

        // This is the "type" of the item, for example, "CPPFile".  This has nothing to
        // do with the file extension or anything else related to the OS.
        // Each project author can define his own item types, which are
        // just a way of bucketing similar items together.
        private string name;

        /// <summary>
        /// The library to consult for possible default metadata values when the
        /// item itself does not have a value for requested metadata.
        /// May be null, if this item is not associated with a particular project.
        /// </summary>
        private ItemDefinitionLibrary itemDefinitionLibrary;

        // This is the "Include" value of a virtual item. This
        // may contain wildcards.
        // For persisted (non-virtual) items, the include is gotten from 
        // the backing xml; there is no point storing a copy here, and this
        // remains null.
        private string include = null;

        // In the project file, the user can put arbitrary meta-data on the item tag. These meta-data are termed "custom item
        // attributes" (NOT to be confused with XML attributes on the item element). For a persisted BuildItem object, these meta-data
        // are stored as child nodes of the "itemElement", and are also cached in this hash table (to allow case-insensitive
        // lookup). For a virtual BuildItem object, the meta-data are only stored in this hash table.
        // NOTE: for a persisted item, it is possible for the same custom attribute to appear multiple times under an item
        // element -- we allow this, but follow a "last one wins" policy
        private CopyOnWriteHashtable unevaluatedCustomMetadata = null;
        private CopyOnWriteHashtable evaluatedCustomMetadata = null;

        // Private copies of the above metadata tables, kept if the metadata has been modified during the build;
        // we can revert to them later to go back to the pre-build state
        private CopyOnWriteHashtable unevaluatedCustomMetadataBackup;
        private CopyOnWriteHashtable evaluatedCustomMetadataBackup;

        // If this item is persisted in the project file, then we need to
        // store a reference to the parent <ItemGroup>.  This makes it easier
        // to remove an item from a project through the object model.
        private BuildItemGroup parentPersistedItemGroup = null;

        // When an item in a project file gets evaluated, it may evaluate to
        // several different items.  For example, in the project file,
        // <Blah Include="a;b;c"/> really evaluates to 3 separate
        // items:  a, b, and c.
        // For each of these 3 evaluated items, the parent item is the original
        // item tag that came from the project file.  When one of these
        // "child" items is modified/deleted/etc. through the object model, we
        // need a pointer to the parent item in order to modify it accordingly.
        private BuildItem parentPersistedItem = null;

        // When an item in a project file gets evaluated, it may evaluate to
        // several different items.  This is the list of child items that
        // a real item tag in the project file evaluated to.
        private BuildItemGroup childItems = null;

        // this is the Include attribute with all embedded properties expanded, but with wildcards intact
        private string evaluatedItemSpecEscaped;

        // This is the final evaluated item specification, and is only valid
        // if you are looking at an item returned from Project.EvaluatedItems.
        // If the "Include" attribute was something simple that represented a
        // single file, like "foo.cs", then this finalItemSpec would also be
        // "foo.cs".  However, in the case of wildcards, the "Include" attribute
        // might say "*.cs", whereas this finalItemSpec would be one single item
        // such as "foo.cs".
        private string finalItemSpecEscaped = String.Empty;

        // this table is used to cache the results of path manipulations performed on the (evaluated/final) item-spec if the
        // item-spec is a true file-spec i.e. it contains file or directory path information
        // NOTE: modifications to the item-spec are typically needed for item vector transforms, but the modifiers are also
        // exposed as pre-defined/reserved attributes on the item
        private Hashtable itemSpecModifiers;

        // the portion of the directory of the final item-spec that was generated by a recursive wildcard
        private string recursivePortionOfFinalItemSpecDirectory = null;

        // If this is a persisted item element, this boolean tells us whether
        // it came from the main project file or an imported project file.
        private bool importedFromAnotherProject = false;

        #endregion

        #region CustomSerializationToStream
        internal void WriteToStream(BinaryWriter writer)
        {
            writer.Write(importedFromAnotherProject);
            #region RecursivePortionOfFinalItemSpecDirectory
            if (recursivePortionOfFinalItemSpecDirectory == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write(recursivePortionOfFinalItemSpecDirectory);
            }
            #endregion
            #region FinalItemSpecEscaped
            if (finalItemSpecEscaped == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write(finalItemSpecEscaped);
            }
            #endregion
            #region Name
            if (name == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write(name);
            }
            #endregion
            #region Include
            string includeValue = this.include;
            if (IsBackedByXml)
            {
                includeValue = xml.Include;
            }
            if (includeValue == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write(includeValue);
            }
            #endregion
            #region EvaluatedItemSpecEscaped
            if (evaluatedItemSpecEscaped == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write(evaluatedItemSpecEscaped);
            }
            #endregion
            #region UnevaluatedCustomMetaData
            IDictionary metadata = GetAllCustomUnevaluatedMetadata();
            if (metadata == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write((Int32)metadata.Count);
                foreach (string key in metadata.Keys)
                {
                    writer.Write(key);
                    if (metadata[key] == null)
                    {
                        writer.Write((byte)0);
                    }
                    else
                    {
                        writer.Write((byte)1);
                        writer.Write((string)metadata[key]);
                    }
                }
            }
            #endregion
            #region EvaluatedCustomMetaData
            metadata = GetAllCustomEvaluatedMetadata();
            if (metadata == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write((Int32)metadata.Count);
                foreach (string key in metadata.Keys)
                {
                    writer.Write(key);
                    if (metadata[key] == null)
                    {
                        writer.Write((byte)0);
                    }
                    else
                    {
                        writer.Write((byte)1);
                        writer.Write((string)metadata[key]);
                    }
                }
            }
            #endregion
            #region ItemSpecModifiers
            if (itemSpecModifiers == null)
            {
                writer.Write((byte)0);
            }
            else
            {
                writer.Write((byte)1);
                writer.Write((Int32)itemSpecModifiers.Count);
                foreach (string key in itemSpecModifiers.Keys)
                {
                    writer.Write(key);
                    if (itemSpecModifiers[key] == null)
                    {
                        writer.Write((byte)0);
                    }
                    else
                    {
                        writer.Write((byte)1);
                        writer.Write((string)itemSpecModifiers[key]);
                    }
                }
            }
            #endregion
        }

        internal void CreateFromStream(BinaryReader reader)
        {
            importedFromAnotherProject = reader.ReadBoolean();
            #region RecursivePortionOfFinalItemSpecDirectory
            if (reader.ReadByte() == 0)
            {
                recursivePortionOfFinalItemSpecDirectory = null;
            }
            else
            {
                recursivePortionOfFinalItemSpecDirectory = reader.ReadString();
            }
            #endregion
            #region FinalItemSpecEscaped
            if (reader.ReadByte() == 0)
            {
                finalItemSpecEscaped = null;
            }
            else
            {
                finalItemSpecEscaped = reader.ReadString();
            }
            #endregion
            #region Name
            if (reader.ReadByte() == 0)
            {
                name = null;
            }
            else
            {
                name = reader.ReadString();
            }
            #endregion
            #region VitrualIncludeAttribute
            if (reader.ReadByte() == 0)
            {
                include = null;
            }
            else
            {
                include = reader.ReadString();
            }
            #endregion
            #region EvaluatedItemSpecEscaped
            if (reader.ReadByte() == 0)
            {
                evaluatedItemSpecEscaped = null;
            }
            else
            {
                evaluatedItemSpecEscaped = reader.ReadString();
            }
            #endregion
            #region UnevaluatedCustomMetadata
            if (reader.ReadByte() == 0)
            {
                unevaluatedCustomMetadata = null;
            }
            else
            {
                int numberUnevaluatedItems = reader.ReadInt32();
                unevaluatedCustomMetadata = new CopyOnWriteHashtable(numberUnevaluatedItems, StringComparer.OrdinalIgnoreCase);
                for (int i = 0; i < numberUnevaluatedItems; i++)
                {
                    string key = reader.ReadString();
                    string value = null;
                    if (reader.ReadByte() != 0)
                    {
                        value = reader.ReadString();
                    }
                    unevaluatedCustomMetadata.Add(key, value);
                }
            }
            #endregion
            #region EvaluatedCustomMetadata
            if (reader.ReadByte() == 0)
            {
                evaluatedCustomMetadata = null;
            }
            else
            {
                int numberevaluatedCustomMetadata = reader.ReadInt32();
                evaluatedCustomMetadata = new CopyOnWriteHashtable(numberevaluatedCustomMetadata, StringComparer.OrdinalIgnoreCase);
                for (int i = 0; i < numberevaluatedCustomMetadata; i++)
                {
                    string key = reader.ReadString();
                    string value = null;
                    if (reader.ReadByte() != 0)
                    {
                        value = reader.ReadString();
                    }
                    evaluatedCustomMetadata.Add(key, value);
                }
            }
            #endregion
            #region ItemSpecModifiers
            if (reader.ReadByte() == 0)
            {
                itemSpecModifiers = null;
            }
            else
            {
                int numberItemSpecModifiers = reader.ReadInt32();
                itemSpecModifiers = new Hashtable(numberItemSpecModifiers);
                for (int i = 0; i < numberItemSpecModifiers; i++)
                {
                    string key = reader.ReadString();
                    string value = null;
                    if (reader.ReadByte() != 0)
                    {
                        value = reader.ReadString();
                    }
                    itemSpecModifiers.Add(key, value);
                }
            }
            #endregion
        }
        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new item with an XML element backing it. Use this to add a new persisted item to the project file.
        /// </summary>
        /// <param name="ownerDocument">can be null</param>
        /// <param name="name">can be null</param>
        internal BuildItem(XmlDocument ownerDocument, string name, string include, ItemDefinitionLibrary itemDefinitionLibrary)
            : this(ownerDocument, name, include, true /* create custom metadata cache */, itemDefinitionLibrary)
        {
        }

        /// <summary>
        /// Creates either a new item with an XML element backing it, or a virtual
        /// item. To conserve memory, this constructor does not allocate storage
        /// for custom metadata, unless told to do so.
        /// </summary>
        /// <remarks>
        /// PERF WARNING: Allocating memory for the custom metadata cache is expensive
        /// when a build generates a large number of items.
        /// </remarks>
        /// <param name="ownerDocument">can be null</param>
        /// <param name="name">can be null</param> 
        private BuildItem(XmlDocument ownerDocument, string name, string include, bool createCustomMetadataCache, ItemDefinitionLibrary itemDefinitionLibrary)
        {
            BuildItemHelper(ownerDocument, name, include, createCustomMetadataCache, itemDefinitionLibrary);
        }

        /// <summary>
        /// Common code for constructors. If an ownerDocument is passed in, it's a persisted element.
        /// </summary>
        /// <param name="itemName">can be null</param>
        /// <param name="itemDefinitionLibrary">can only be null if ownerDocument is null</param>
        private void BuildItemHelper(XmlDocument ownerDocument, string itemName, string itemInclude, bool createCustomMetadataCache, ItemDefinitionLibrary itemDefinitionLibraryToUse)
        {
            // Only check for null. It's legal to make BuildItems with empty
            // item specs -- this is to be consistent with how we shipped TaskItem.
            // See #567058.
            ErrorUtilities.VerifyThrowArgumentNull(itemInclude, nameof(itemInclude));

            // Validate that the item name doesn't contain any illegal characters.
            if (itemName != null)
            {
                XmlUtilities.VerifyThrowValidElementName(itemName);
                ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[itemName] == null, "CannotModifyReservedItem", itemName);
            }

            // If no owner document was passed in, then it's not going to have an
            // XML element backing it.
            if (ownerDocument == null)
            {
                this.include = itemInclude;
                this.isPartOfProjectManifest = false;
            }
            else
            {
                ErrorUtilities.VerifyThrowArgumentLength(itemName, "itemType");
                MustHaveItemDefinitionLibrary(itemDefinitionLibraryToUse);

                // The caller has given us an owner document, so we're going to create a
                // new item element associated with that document.  The new item element
                // doesn't actually get added to the XML document automatically.
                this.xml = new BuildItemGroupChildXml(ownerDocument, itemName, itemInclude);
                this.isPartOfProjectManifest = true;
            }

            if (createCustomMetadataCache)
            {
                // PERF NOTE: only create cache if told to do so, because creating
                // this cache for a large number of items is expensive
                InitializeCustomMetadataCache();
            }

            this.name = itemName;

            // The evaluated and final item specs start off initialized to the "Include" attribute.
            this.evaluatedItemSpecEscaped = itemInclude;
            this.finalItemSpecEscaped = itemInclude;

            this.importedFromAnotherProject = false;
            this.itemDefinitionLibrary = itemDefinitionLibraryToUse;
        }

        /// <summary>
        /// This constructor creates a new virtual (non-persisted) item with the
        /// specified type and include.
        /// </summary>
        public BuildItem(string itemName, string itemInclude) :
            this(null /* no XML */, itemName, itemInclude, null /* no item definition library */)
        {
        }

        /// <summary>
        /// This constructor initializes a persisted item from an existing item
        /// element which exists either in the main project file or one of the
        /// imported files.
        /// </summary>
        internal BuildItem(XmlElement itemElement, bool importedFromAnotherProject, ItemDefinitionLibrary itemDefinitionLibrary)
            : this(itemElement, importedFromAnotherProject, true /* part of project manifest */, itemDefinitionLibrary)
        {
        }

        /// <summary>
        /// This constructor initializes an item from an item element.
        /// It is part of the project manifest or not as specified.
        /// </summary>
        internal BuildItem(XmlElement itemElement, bool importedFromAnotherProject, bool isPartOfProjectManifest, ItemDefinitionLibrary itemDefinitionLibrary)
        {
            MustHaveItemDefinitionLibrary(itemDefinitionLibrary);

            InitializeFromItemElement(itemElement);
            this.importedFromAnotherProject = importedFromAnotherProject;
            this.isPartOfProjectManifest = isPartOfProjectManifest;
            this.itemDefinitionLibrary = itemDefinitionLibrary;

            ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[name] == null, ItemElement, "CannotModifyReservedItem", name);
        }

        /// <summary>
        /// This constructor creates a new virtual (non-persisted) item based
        /// on a ITaskItem object that was emitted by a task.
        /// </summary>
        public BuildItem(string itemName, ITaskItem taskItem) 
        {
            ErrorUtilities.VerifyThrowArgumentNull(taskItem, nameof(taskItem));

            string itemInclude = EscapingUtilities.Escape(taskItem.ItemSpec);

            BuildItemHelper
                (
                null /* this is a virtual item with no backing XML */,
                itemName,
                itemInclude,
                false, /* PERF NOTE: don't waste time creating a new custom metadata cache,
                       * because we're going to clone the given task item's custom metadata */
                null /* no definition library */
                );

            IDictionary rawSourceTable = taskItem.CloneCustomMetadata();

            // Go through and escape the metadata as necessary.
            string[] keys = new string[rawSourceTable.Count];
            rawSourceTable.Keys.CopyTo(keys, 0);
            foreach (string singleMetadataName in keys)
            {
                string singleMetadataValue = (string) rawSourceTable[singleMetadataName];
                rawSourceTable[singleMetadataName] = EscapingUtilities.Escape(singleMetadataValue);
            }

            this.unevaluatedCustomMetadata = new CopyOnWriteHashtable(rawSourceTable, StringComparer.OrdinalIgnoreCase);
            this.evaluatedCustomMetadata = new CopyOnWriteHashtable(rawSourceTable, StringComparer.OrdinalIgnoreCase);
            this.isPartOfProjectManifest = false;
        }

        #endregion

        #region Properties

        /// <summary>
        /// This returns a boolean telling you whether this particular item
        /// was imported from another project, or whether it was defined
        /// in the main project.  For virtual items which have no
        /// persistence, this is false.
        /// </summary>
        public bool IsImported
        {
            get { return importedFromAnotherProject; }
        }

        /// <summary>
        /// Accessor for the item's "type" string.  Note that changing the "Type"
        /// of an BuildItem requires the whole project to be re-evalauted.  This is because
        /// items are frequently stored in hash tables based on their item types,
        /// so changing an item type would mess up the tables.  In the current
        /// implementation the caller who changes the item type is responsible
        /// for calling Project.MarkAsDirty().
        /// </summary>
        public string Name
        {
            get
            {
                ErrorUtilities.VerifyThrow(name != null, "Get Name: Item has not been initialized.");
                return this.name;
            }

            set
            {
                ErrorUtilities.VerifyThrow(name != null, "Set Name: Item has not been initialized.");
                ErrorUtilities.VerifyThrowArgumentLength(value, "Name");
                XmlUtilities.VerifyThrowValidElementName(value);
                MustNotBeImported();
                ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[value] == null, "CannotModifyReservedItem", value);

                SplitChildItemIfNecessary();
                this.name = value;

                if (IsBackedByXml)
                {
                    xml.Name = value;

                    // Because we actually have a new XML element representing this item now, we
                    // have to update our parent items and child items.  Most other modifications
                    // to an item don't require this, because they are simply modifying the XML
                    // element.  But here, because the item type is the XML element name, we actually
                    // had to create a new XML element with the new item type.  This was done by the
                    // RenameXmlElement method, called above.
                    if (this.ParentPersistedItem != null)
                    {
                        ParentPersistedItem.UpdateBackingXml(this.xml);
                    }

                    if (this.childItems != null)
                    {
                        foreach (BuildItem childItem in this.childItems)
                        {
                            childItem.UpdateBackingXml(this.xml);
                        }
                    }
                }

                MarkItemAsDirty();
            }
        }

        /// <summary>
        /// The backing library of default metadata values, if any.
        /// Projects need to update this with their own library,
        /// when an item is added to them.
        /// </summary>
        internal ItemDefinitionLibrary ItemDefinitionLibrary
        {
            get { return itemDefinitionLibrary; }
            set { itemDefinitionLibrary = value; }
        }

        /// Accessor for the item's "include" string.
        /// </summary>
        /// <owner>RGoel</owner>
        public string Include
        {
            get
            {
                if (IsBackedByXml)
                {
                    return xml.Include;
                }
                else if (include != null)
                {
                    return include;
                }
                else
                {
                    ErrorUtilities.ThrowInternalError("Item has not been initialized.");
                    return null;
                }
            }

            set
            {
                ErrorUtilities.VerifyThrowArgument(value != null, "NullIncludeNotAllowed", XMakeAttributes.include);
                MustNotBeImported();

                if (IsBackedByXml)
                {
                    // If this is an evaluated item that originated from the project file, and the original
                    // item is declared using a wildcard that still matches the new item spec ...
                    if ((ParentPersistedItem?.NewItemSpecMatchesExistingWildcard(value) == true))
                    {
                        // Don't need to touch the project file since the original wildcard still matches
                        // the new item spec.  But it still should be reevaluated the next time around.
                        MarkItemAsDirtyForReevaluation();
                    }
                    else
                    {
                        SplitChildItemIfNecessary();
                        xml.Include = value;
                        MarkItemAsDirty();
                    }
                }
                else if (this.include != null)
                {
                    this.include = value;
                    MarkItemAsDirty();
                }
                else
                {
                    ErrorUtilities.VerifyThrow(false, "Item has not been initialized.");
                }

                // The evaluated and final item specs start off initialized to the "Include" attribute.
                this.evaluatedItemSpecEscaped = value;
                this.finalItemSpecEscaped = value;
            }
        }

        /// <summary>
        /// Gets the names of metadata on the item -- also includes the pre-defined/reserved item-spec modifiers.
        /// </summary>
        /// <owner>SumedhK, JomoF</owner>
        /// <value>Collection of name strings.</value>
        public ICollection MetadataNames
        {
            get
            {
                // Add all the custom metadata.
                List<string> list = GetAllCustomMetadataNames();

                // Add all the built-in metadata.
                list.AddRange(FileUtilities.ItemSpecModifiers.All);

                return list;
            }
        }

        /// <summary>
        /// Gets the number of metadata set on the item.
        /// </summary>
        /// <owner>SumedhK</owner>
        /// <value>Count of metadata.</value>
        public int MetadataCount
        {
            get
            {
                return GetCustomMetadataCount() + FileUtilities.ItemSpecModifiers.All.Length;
            }
        }

        /// <summary>
        /// Gets the names of metadata on the item -- also includes the pre-defined/reserved item-spec modifiers.
        /// </summary>
        /// <owner>SumedhK, JomoF</owner>
        /// <value>Collection of name strings.</value>
        public ICollection CustomMetadataNames
        {
            get
            {
                // All the custom metadata.
                return GetAllCustomMetadataNames();
            }
        }

        /// <summary>
        /// Gets the number of metadata set on the item.
        /// </summary>
        /// <owner>SumedhK</owner>
        /// <value>Count of metadata.</value>
        public int CustomMetadataCount
        {
            get
            {
                return GetCustomMetadataCount();
            }
        }
        
        /// <summary>
        /// Read-only accessor for accessing the XML attribute for "Include".  Callers should
        /// never try and modify this.  Go through this.Include to change the include spec.
        /// </summary>
        internal XmlAttribute IncludeAttribute
        {
            get { return IsBackedByXml ? xml.IncludeAttribute : null; }
        }

        /// <summary>
        /// Accessor for the item's "exclude" string.
        /// </summary>
        /// <owner>RGoel</owner>
        public string Exclude
        {
            get { return IsBackedByXml ? xml.Exclude : String.Empty; }

            set
            {
                // We don't support having an "Exclude" for virtual items.  Only persisted items can have an "Exclude".
                ErrorUtilities.VerifyThrowInvalidOperation(IsBackedByXml, "CannotSetExcludeOnVirtualItem", XMakeAttributes.exclude);

                MustNotBeImported();
                ErrorUtilities.VerifyThrowInvalidOperation(this.ParentPersistedItem == null, "CannotSetExcludeOnEvaluatedItem", XMakeAttributes.exclude);

                xml.Exclude = value;
                MarkItemAsDirty();
            }
        }

        /// <summary>
        /// Read-only accessor for accessing the XML attribute for "Exclude".  Callers should
        /// never try and modify this.  Go through this.Exclude to change the exclude spec.
        /// </summary>
        /// <owner>RGoel</owner>
        internal XmlAttribute ExcludeAttribute
        {
            get { return IsBackedByXml ? xml.ExcludeAttribute : null; }
        }

        /// <summary>
        /// Accessor for the item's "condition".
        /// </summary>
        /// <owner>RGoel</owner>
        public string Condition
        {
            get { return IsBackedByXml ? xml.Condition : String.Empty; }

            set
            {
                // If this BuildItem object is not actually represented by an
                // XML element in the project file, then do not allow
                // the caller to set the condition.
                ErrorUtilities.VerifyThrowInvalidOperation(IsBackedByXml, "CannotSetCondition");

                MustNotBeImported();

                SplitChildItemIfNecessary();

                xml.Condition = value;
                MarkItemAsDirty();
            }
        }

        /// <summary>
        /// Read-only accessor for accessing the XML attribute for "Condition".  Callers should
        /// never try and modify this.  Go through this.Condition to change the condition.
        /// </summary>
        /// <owner>RGoel</owner>
        internal XmlAttribute ConditionAttribute
        {
            get { return IsBackedByXml ? xml.ConditionAttribute : null; }
        }

        /// <summary>
        /// Gets the XmlElement representing this item.
        /// </summary>
        /// <owner>RGoel</owner>
        /// <value>The item XmlElement, or null if item is virtual.</value>
        internal XmlElement ItemElement
        {
            get { return IsBackedByXml ? xml.Element : null; }
        }

        /// <summary>
        /// Accessor for the final evaluated item specification.  This is read-only.
        /// </summary>
        /// <owner>RGoel</owner>
        internal string FinalItemSpecEscaped
        {
            get { return finalItemSpecEscaped; }
        }

        /// <summary>
        /// Returns the unescaped final value of the item.
        /// </summary>
        /// <owner>RGoel</owner>
        public string FinalItemSpec
        {
            get { return EscapingUtilities.UnescapeAll(FinalItemSpecEscaped); }
        }
        
        /// <summary>
        /// Read-only accessor for the piece of the item's Include that resulted in
        /// this item, with properties expanded.
        /// </summary>
        internal string EvaluatedItemSpec
        {
            get { return evaluatedItemSpecEscaped; }
        }

        /// <summary>
        /// If this item is persisted in the project file, then we need to
        /// store a reference to the parent &lt;ItemGroup&gt;.  This makes it easier
        /// to remove an item from a project through the object model.
        /// </summary>
        /// <owner>RGoel</owner>
        internal BuildItemGroup ParentPersistedItemGroup
        {
            get { return parentPersistedItemGroup; }

            set
            {
                ErrorUtilities.VerifyThrow( ((value == null) && (this.parentPersistedItemGroup != null)) || ((value != null) && (this.parentPersistedItemGroup == null)),
                    "Either new parent cannot be assigned because we already have a parent, or old parent cannot be removed because none exists.");

                this.parentPersistedItemGroup = value;
            }
        }

        /// <summary>
        /// When an item in a project file gets evaluated, it may evaluate to
        /// several different items.  For example, in the project file,
        /// &lt;Blah Include="a;b;c"/&gt; really evaluates to 3 separate
        /// items:  a, b, and c.
        /// For one of these 3 evaluated items, the parent item is the original
        /// item tag that came from the project file.  When one of these
        /// "child" items is modified/deleted/etc. through the object model, we
        /// need a pointer to the parent item in order to modify it accordingly.
        /// </summary>
        /// <owner>rgoel</owner>
        internal BuildItem ParentPersistedItem
        {
            get { return parentPersistedItem; }

            set
            {
                ErrorUtilities.VerifyThrow( ((value == null) && (this.parentPersistedItem != null)) || ((value != null) && (this.parentPersistedItem == null)),
                    "Either new parent cannot be assigned because we already have a parent, or old parent cannot be removed because none exists.");

                this.parentPersistedItem = value;
            }
        }

        /// <summary>
        /// When an item in a project file gets evaluated, it may evaluate to
        /// several different items.  This is the list of child items that
        /// a real item tag in the project file evaluated to.
        /// </summary>
        /// <owner>RGoel</owner>
        internal BuildItemGroup ChildItems
        {
            get
            {
                if (this.childItems == null)
                {
                    this.childItems = new BuildItemGroup ();
                }

                return this.childItems;
            }
        }

        internal bool IsUninitializedItem
        {
            get { return this.name == null; }
        }

        /// <summary>
        /// Whether this item is part of the project "manifest", ie., it is defined in XML
        /// outside of a target.
        /// </summary>
        internal bool IsPartOfProjectManifest
        {
            get { return isPartOfProjectManifest; }
        }

        /// <summary>
        /// Whether this item has backing XML
        /// </summary>
        internal bool IsBackedByXml
        {
            get { return xml != null; }
        }

        /// <summary>
        /// Whether the metadata lists have been backed up
        /// </summary>
        internal bool IsBackedUp
        {
            get { return unevaluatedCustomMetadataBackup != null; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Get the collection of custom metadata. This does not include built-in metadata.
        /// </summary>
        /// <remarks>
        /// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS:
        /// 1) this method should return a clone of the metadata
        /// 2) writing to this dictionary should not be reflected in the underlying item.
        /// </remarks>
        internal IDictionary CloneCustomMetadata() 
        {
            IDictionary result = (IDictionary)this.evaluatedCustomMetadata.Clone();
            return MergeDefaultMetadata(result);
        }

        /// <summary>
        /// Initializes the cache for storing custom attributes (meta-data).
        /// </summary>
        private void InitializeCustomMetadataCache()
        {
            this.unevaluatedCustomMetadata = new CopyOnWriteHashtable(StringComparer.OrdinalIgnoreCase);
            this.evaluatedCustomMetadata = new CopyOnWriteHashtable(StringComparer.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Initializes a persisted item from an existing item element which exists either in the main project file or in one of
        /// the imported files.
        /// </summary>
        /// <param name="itemElementToParse"></param>
        private void InitializeFromItemElement(XmlElement element)
        {
            this.xml = new BuildItemGroupChildXml(element, ChildType.BuildItemAdd);

            this.name = xml.Name;
            this.itemSpecModifiers = null;
            this.recursivePortionOfFinalItemSpecDirectory = null;

            this.evaluatedItemSpecEscaped = xml.Include;
            this.finalItemSpecEscaped = xml.Include;

            InitializeCustomMetadataCache();
        }

        /// <summary>
        /// Allows the project to save the value of the item's Include attribute after expanding all embedded properties.
        /// </summary>
        /// <param name="evaluatedItemSpecValueEscaped">Can be null.</param>
        internal void SetEvaluatedItemSpecEscaped(string evaluatedItemSpecValueEscaped)
        {
            this.evaluatedItemSpecEscaped = evaluatedItemSpecValueEscaped;
        }

        /// <summary>
        /// This allows the engine to set the final item spec for this item, after
        /// it has been evaluated.  A fair question is ... why doesn't the BuildItem
        /// object itself have an internal Evaluate() method which allows it
        /// to evaluate itself and set its own finalItemSpec.  The answer is
        /// simply that the BuildItem doesn't have nearly enough information to be
        /// able to do this.  An BuildItem can only be evaluated in the context of the
        /// larger project.
        /// </summary>
        internal void SetFinalItemSpecEscaped
        (
            string finalItemSpecValueEscaped
        )
        {
            this.finalItemSpecEscaped = finalItemSpecValueEscaped;
            this.itemSpecModifiers = null;
            this.recursivePortionOfFinalItemSpecDirectory = null;
        }

        /// <summary>
        /// This method extracts from the final item-spec the portion of its directory that matches the recursive wildcard
        /// specification (if any) given in the original item-spec i.e. in the "Include" attribute.
        ///
        /// For the path:
        ///
        ///     subdir1\**\debug\*.txt
        ///
        /// Recursive portion:
        ///
        ///     bin\debug\
        ///
        /// </summary>
        /// <returns>recursively matched portion of item directory</returns>
        internal string ExtractRecursivePortionOfFinalItemSpecDirectory()
        {
            if (recursivePortionOfFinalItemSpecDirectory == null)
            {
                recursivePortionOfFinalItemSpecDirectory = String.Empty;

                // The RecursiveDir may have come from a TaskCreated item.
                // In this case, use that instead of deriving it.
                if (this.unevaluatedCustomMetadata[FileUtilities.ItemSpecModifiers.RecursiveDir] != null)
                {
                    recursivePortionOfFinalItemSpecDirectory = (string)this.unevaluatedCustomMetadata[FileUtilities.ItemSpecModifiers.RecursiveDir];
                }
                else
                {
                    // only do this for items whose evaluated Include value is available -- virtual items do not have an Include
                    // attribute, so it's usually impossible to compute the recursive path (unless the virtual item is a clone of an
                    // item that has backing XML)
                    if (evaluatedItemSpecEscaped != null)
                    {
                        // Now we are comparing values that are "canonically escaped" by us -- the "FinalItemSpecEscaped", which comes from matching against the file system
                        // with values that may be escaped somewhat, or not at all -- the "evaluatedItemSpecEscaped", which comes from the literal Include in the XML.
                        // Anything escaped in that literal Include in the XML should be treated as literal. For example "%28" in the XML should match "(" in the file system.
                        // To compare like with like, unescape both first.
                        FileMatcher.Result match = FileMatcher.FileMatch(EscapingUtilities.UnescapeAll(evaluatedItemSpecEscaped), EscapingUtilities.UnescapeAll(FinalItemSpecEscaped));

                        // even if the evaluated Include value is a legal file spec, it may not match the final item-spec for various
                        // reasons e.g. Include is a semi-colon separated list, or it is an unsupported //server/share pattern, etc.
                        if (match.isLegalFileSpec && match.isMatch)
                        {
                            recursivePortionOfFinalItemSpecDirectory = match.wildcardDirectoryPart;
                        }
                    }
                }
            }

            return recursivePortionOfFinalItemSpecDirectory;
        }

        /// <summary>
        /// Evaluates the item and returns a virtual group containing any resulting items.
        /// This allows an item to be evaluated without it belonging to an item group.
        /// </summary>
        internal BuildItemGroup Evaluate(Expander expander, 
                                        string baseDirectory, 
                                        bool expandMetadata,
                                        ParserOptions parserOptions,
                                        EngineLoggingServices loggingServices, 
                                        BuildEventContext buildEventContext)
        {
            BuildItemGroup result = new BuildItemGroup();
            bool itemCondition = Utilities.EvaluateCondition(Condition,
                                                             ConditionAttribute,
                                                             expander,
                                                             parserOptions,
                                                             loggingServices,
                                                             buildEventContext);
            if (!itemCondition)
            {
                return result;
            }

            EvaluateAllItemMetadata(expander, parserOptions, loggingServices, buildEventContext);
            BuildItemGroup items = BuildItemGroup.ExpandItemIntoItems(baseDirectory, this, expander, expandMetadata);

            for (int i = 0; i < items.Count; i++)
            {
                BuildItem newItem = CreateClonedParentedItem(items[i], this);
                result.AddExistingItem(newItem);
            }

            return result;
        }

        /// <summary>
        /// Create a clone of the parent item with all the information from the child item.
        /// </summary>
        /// <remarks>
        /// FUTURE: It is unclear what this Whidbey code is for -- the callers already have a child item don't they?
        /// Can this be eliminated to avoid excess cloning?
        /// </remarks>
        internal static BuildItem CreateClonedParentedItem(BuildItem childItem, BuildItem parentItem)
        {
            BuildItem newItem = parentItem.Clone();
            newItem.SetEvaluatedItemSpecEscaped(childItem.EvaluatedItemSpec);
            newItem.SetFinalItemSpecEscaped(childItem.FinalItemSpecEscaped);

            // If this item was defined based on another item, grab that other
            // item's attributes.
            newItem.CloneVirtualMetadata();
            newItem.ImportVirtualMetadataFrom(childItem);

            // BUT if this item itself has attributes declared in the project file,
            // then these should clearly win.
            newItem.ImportVirtualMetadataFrom(parentItem);

            // Set up the parent/child relationship.  The "parent" is the actual item
            // tag as declared in the project file.  The "child" is the item that
            // it evaluated to.  For example, a parent <Blah Include="*.cs"/>
            // may evaluate to many children <Blah Include="a.cs"/>, b.cs, c.cs, etc.
            newItem.ParentPersistedItem = parentItem;
            return newItem;
        }

        /// <summary>
        /// Populate the lists of evaluated and unevaluated metadata with all metadata that have true conditions.
        /// </summary>
        /// <remarks>
        /// FUTURE: Currently this isn't done when the item is constructed; so for example HasMetadata will always return
        /// false until EvaluatedAllItemMetadata is explicitly called. The reason for this is that Metadata are
        /// not first class objects, they are merely string pairs produced by running over the child XML with a particular expander.
        /// When Metadata are first class objects this method can be changed to merely evaluate them, 
        /// just as BuildItemGroup.Evaluate does for BuildItem, then methods like HasMetadata behave more sanely. Of course this
        /// could be a breaking change.
        /// </remarks>
        internal void EvaluateAllItemMetadata
        (
            Expander expander,
            ParserOptions parserOptions,
            EngineLoggingServices loggingServices,
            BuildEventContext buildEventContext
        )
        {
            ErrorUtilities.VerifyThrow(expander != null, "Null expander passed in.");

            // Cache all custom attributes on the item. For a persisted item, this will walk the item's child nodes, and
            // cache the custom attributes using a "last one wins" policy. For a virtual item, this method does nothing.

            // We only evaluate metadata by reading XML
            if (IsBackedByXml)
            {
                ErrorUtilities.VerifyThrow((this.evaluatedCustomMetadata != null) && (this.unevaluatedCustomMetadata != null),
                    "Item is not initialized properly.");

                // We're evaluating from scratch, so clear out any old cached attributes.
                this.evaluatedCustomMetadata.Clear();
                this.unevaluatedCustomMetadata.Clear();

                // Let the expander know about our own item type, so it can
                // expand unqualified metadata expressions
                SpecificItemDefinitionLibrary specificItemDefinitionLibrary = new SpecificItemDefinitionLibrary(name, itemDefinitionLibrary);
                expander = new Expander(expander, specificItemDefinitionLibrary);

                List<XmlElement> metadataElements = xml.GetChildren();

                // look at all the item's child nodes
                foreach (XmlElement metadataElement in metadataElements)
                {
                    // confirm that the child node is not conditionally disabled
                    bool condition = true;
                    XmlAttribute conditionAttribute = ProjectXmlUtilities.GetConditionAttribute(metadataElement, true /*no other attributes allowed*/);

                    if (conditionAttribute != null)
                    {
                        condition = Utilities.EvaluateCondition(conditionAttribute.Value,
                            conditionAttribute, expander, null, parserOptions,
                            loggingServices, buildEventContext);
                    }

                    if (condition)
                    {
                        // cache its value, both the evaluated and unevaluated.
                        string unevaluatedMetadataValue = Utilities.GetXmlNodeInnerContents(metadataElement);
                        unevaluatedCustomMetadata[metadataElement.Name] = unevaluatedMetadataValue;
                        string evaluatedMetadataValue = expander.ExpandAllIntoStringLeaveEscaped(unevaluatedMetadataValue, metadataElement);
                        evaluatedCustomMetadata[metadataElement.Name] = evaluatedMetadataValue;

                        // Add this metadata to the running table we're using, so that one piece of metadata can refer to another one above
                        expander.SetMetadataInMetadataTable(name, metadataElement.Name, evaluatedMetadataValue);
                    }
                }
            }
        }

        /// <summary>
        /// Indicates if the given metadata is set on the item.
        /// </summary>
        /// <remarks>BuildItem-spec modifiers are treated as metadata.</remarks>
        public bool HasMetadata(string metadataName)
        {
            ErrorUtilities.VerifyThrowArgumentLength(metadataName, nameof(metadataName));
            ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomAttributes is null.");
#if DEBUG
            // The hashtable of metadata (this.unevaluatedCustomMetadata) should never contain 
            // values for those reserved metadata that the engine provides (Filename, RelativeDir,
            // Extension, etc.).  The one exception is that the hashtable is allowed to contain
            // a value for "RecursiveDir" because that one is extra special ... tasks are allowed
            // to set that one, because it's the only way for tasks (CreateItem) to provide that value.
            // Hence the need to call FileUtilities.IsDerivableItemSpecModifider.
            ErrorUtilities.VerifyThrow(!unevaluatedCustomMetadata.ContainsKey(metadataName) || !FileUtilities.IsDerivableItemSpecModifier(metadataName),
                "Item-spec modifiers are reserved attributes and cannot be redefined -- this should have been caught when the item was initialized.");
#endif

            if (FileUtilities.IsItemSpecModifier(metadataName))
            {
                return true;
            }

            if (unevaluatedCustomMetadata.ContainsKey(metadataName))
            {
                return true;
            }

            if (GetDefaultMetadataValue(metadataName) != null)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Retrieves an arbitrary unevaluated metadata value from the item element. These are pieces of metadata that the project author has
        /// placed on the item element that have no meaning to MSBuild. They are just arbitrary metadata that travel around with
        /// the BuildItem wherever it goes.
        /// </summary>
        /// <param name="metadataName">The name of the metadata to retrieve.</param>
        /// <returns>The value of the requested metadata.</returns>
        /// <exception cref="InvalidOperationException">Thrown when the requested metadata is not applicable to the item.</exception>
        public string GetMetadata(string metadataName)
        {
            string metadataValue;
            if (FileUtilities.IsItemSpecModifier(metadataName))
            {
                // BUGBUG VSWhidbey 377466.  If this method is being called directly by an OM
                // consumer, then he is in control of the current working directory.  This
                // means that if he requests the "FullPath" attribute, we will likely compute
                // it incorrectly (and furthermore cache the incorrect value for later use
                // during the build).  What we need to do is make sure to set the current working
                // directory to the directory of the project file before attempting to compute
                // things like "FullPath" or "RootDir".
                metadataValue = GetItemSpecModifier(metadataName);
            }
            else
            {
                ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly.  unevaluatedCustomMetadata is null.");

                metadataValue = (string) this.unevaluatedCustomMetadata[metadataName];
            }

            // If we don't have an explicit value for this metadata then try to find a default value
            if (metadataValue == null)
            {
                metadataValue = GetDefaultMetadataValue(metadataName);
            } 

            return metadataValue ?? String.Empty;
        }

        /// <summary>
        /// Retrieves an arbitrary metadata from the item element, expands any property and item references within it, and 
        /// unescapes it.
        /// </summary>
        /// <remarks>Custom attributes on virtual items are not evaluated.</remarks>
        /// <param name="metadataName">The name of the attribute to retrieve.</param>
        /// <returns>The evaluated value of the requested attribute.</returns>
        /// <exception cref="InvalidOperationException">Thrown when the requested attribute is not applicable to the item.</exception>
        public string GetEvaluatedMetadata(string metadataName)
        {
            return EscapingUtilities.UnescapeAll(this.GetEvaluatedMetadataEscaped(metadataName));
        }

        /// <summary>
        /// Retrieves an arbitrary metadata from the item element, expands any property and item references within it.
        /// </summary>
        /// <remarks>Custom attributes on virtual items are not evaluated.</remarks>
        /// <param name="metadataName">The name of the attribute to retrieve.</param>
        /// <returns>The evaluated value of the requested attribute.</returns>
        /// <exception cref="InvalidOperationException">Thrown when the requested attribute is not applicable to the item.</exception>
        internal string GetEvaluatedMetadataEscaped(string metadataName)
        {
            string metadataValue;
            if (FileUtilities.IsItemSpecModifier(metadataName))
            {
                // BUGBUG VSWhidbey 377466.  If this method is being called directly by an OM
                // consumer, then he is in control of the current working directory.  This
                // means that if he requests the "FullPath" attribute, we will likely compute
                // it incorrectly (and furthermore cache the incorrect value for later use
                // during the build).  What we need to do is make sure to set the current working
                // directory to the directory of the project file before attempting to compute
                // things like "FullPath" or "RootDir".
                metadataValue = GetItemSpecModifier(metadataName);
            }
            else
            {
                ErrorUtilities.VerifyThrow(this.evaluatedCustomMetadata != null, "Item not initialized properly.  evaluatedCustomMetadata is null.");

                metadataValue = (string) this.evaluatedCustomMetadata[metadataName];
            }

            // If we don't have an explicit value for this metadata then try to find a default value
            if (metadataValue == null)
            {
                metadataValue = GetDefaultMetadataValue(metadataName);
            }              

            return metadataValue ?? String.Empty;
        }

        /// <summary>
        /// Retrieves all user-defined/custom attribute names.
        /// </summary>
        internal List<string> GetAllCustomMetadataNames()
        {
            ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");

            List<string> names = new List<string>(GetCustomMetadataCount());

            foreach (string name in unevaluatedCustomMetadata.Keys)
            {
                names.Add(name);
            }

            if (itemDefinitionLibrary != null)
            {
                ICollection<string> defaultedNames = itemDefinitionLibrary.GetDefaultedMetadataNames(name);
                if (defaultedNames != null)
                {
                    names.AddRange(defaultedNames);
                }
            }

            return names;
        }

        /// <summary>
        /// Indicates how many custom attributes are set on this item.
        /// </summary>
        /// <remarks>
        /// This method does NOT count the pre-defined/reserved item-spec modifiers because they are NOT "custom" attributes.
        /// </remarks>
        internal int GetCustomMetadataCount()
        {
            ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");

            int count = unevaluatedCustomMetadata.Count;

            if (itemDefinitionLibrary != null)
            {
                count += itemDefinitionLibrary.GetDefaultedMetadataCount(name);
            }

            return count;
        }

        /// <summary>
        /// Copies all custom attributes to given item.
        /// </summary>
        /// <param name="destinationItem">BuildItem to copy custom attributes to</param>
        public void CopyCustomMetadataTo(BuildItem destinationItem)
        {
            ErrorUtilities.VerifyThrowArgumentNull(destinationItem, nameof(destinationItem));

            if (IsBackedByXml)
            {
                List<XmlElement> children = xml.GetChildren();

                foreach (XmlElement child in children)
                {
                    destinationItem.SetMetadata(child.Name, child.InnerXml);
                }
            }
            else
            {
                ErrorUtilities.VerifyThrow(this.evaluatedCustomMetadata != null, "Item not initialized properly.");

                foreach (DictionaryEntry customAttributeEntry in this.evaluatedCustomMetadata)
                {
                    destinationItem.SetMetadata((string)customAttributeEntry.Key, (string)customAttributeEntry.Value);
                }
            }
        }

        /// <summary>
        /// Clones the hashtables which cache the values of all the custom metadata on this item.
        /// Callers should do this when they know that they have a shallow clone of another item,
        /// and they want to modify the attributes on this item without touching the original item.
        /// </summary>
        /// <owner>RGoel</owner>
        internal void CloneVirtualMetadata()
        {
            this.evaluatedCustomMetadata = (CopyOnWriteHashtable)this.evaluatedCustomMetadata.Clone();
            this.unevaluatedCustomMetadata = (CopyOnWriteHashtable)this.unevaluatedCustomMetadata.Clone();
        }

        /// <summary>
        /// Without altering the backing XML of this item, copy custom metadata from the given item.
        /// </summary>
        /// <param name="itemToCopyFrom"></param>
        internal void ImportVirtualMetadataFrom(BuildItem itemToCopyFrom)
        {
            foreach (DictionaryEntry customAttributeEntry in itemToCopyFrom.evaluatedCustomMetadata)
            {
                this.evaluatedCustomMetadata[customAttributeEntry.Key] = customAttributeEntry.Value;
            }

            foreach (DictionaryEntry customAttributeEntry in itemToCopyFrom.unevaluatedCustomMetadata)
            {
                this.unevaluatedCustomMetadata[customAttributeEntry.Key] = customAttributeEntry.Value;
            }

            Dictionary<string, string> itemDefinitionMetadata = null;

            if (!String.Equals(itemToCopyFrom.Name, this.Name, StringComparison.OrdinalIgnoreCase) &&
                itemDefinitionLibrary.IsEvaluated)
            {
                // copy item definition metadata over as "real" metadata
                itemDefinitionMetadata = itemDefinitionLibrary.GetDefaultedMetadata(itemToCopyFrom.Name);
            }

            if (itemDefinitionMetadata != null)
            {
                foreach (KeyValuePair<string, string> itemDefinitionMetadatum in itemDefinitionMetadata)
                {
                    this.evaluatedCustomMetadata[itemDefinitionMetadatum.Key] = itemDefinitionMetadatum.Value;
                }
            }
        }

        /// <summary>
        /// Retrieves all user-defined/custom metadata after expanding any property references they might contain.
        /// NOTE: Merges them into any default metadata -- so if there are any default metadata, this may return considerably
        /// more than just the metadata specifically on this item.
        /// </summary>
        /// <remarks>
        /// This method does NOT return the pre-defined/reserved item-spec modifiers because they are NOT "custom" metadata
        /// </remarks>
        internal IDictionary GetAllCustomEvaluatedMetadata()
        {
            ErrorUtilities.VerifyThrow(this.evaluatedCustomMetadata != null, "Item not initialized properly. evaluatedCustomMetadata is null.");

            IDictionary result = MergeDefaultMetadata(evaluatedCustomMetadata);

            return result;
        }

        /// <summary>
        /// Retrieves all user-defined/custom metadata in unevaluated form.
        /// NOTE: Merges them into any default metadata -- so if there are any default metadata, this may return considerably
        /// more than just the metadata specifically on this item.
        /// </summary>
        private IDictionary GetAllCustomUnevaluatedMetadata()
        {
            ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");

            IDictionary result = MergeDefaultMetadata(unevaluatedCustomMetadata);

            return result;
        }

        /// <summary>
        /// Returns a dictionary containing any default metadata, with the provided set of metadata
        /// overlaid on those defaults.
        /// </summary>
        private IDictionary MergeDefaultMetadata(IDictionary customMetadata)
        {
            IDictionary result = customMetadata;

            if (itemDefinitionLibrary != null)
            {
                IDictionary defaultedMetadata = itemDefinitionLibrary.GetDefaultedMetadata(name);

                if (defaultedMetadata != null)
                {
                    // If we have any defaulted metadata, return a dictionary containing both specific and defaulted
                    // metadata, with the specific metadata winning if there's a conflict.
                    result = new CopyOnWriteHashtable(defaultedMetadata, StringComparer.OrdinalIgnoreCase);

                    foreach (DictionaryEntry metadata in customMetadata)
                    {
                        result[metadata.Key] = metadata.Value;
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Sets custom metadata on this item, with the option of treating the metadata value
        /// literally, meaning that special sharacters will be escaped.
        /// Does not backup metadata before making changes.
        /// </summary>
        public void SetMetadata(string metadataName, string metadataValue, bool treatMetadataValueAsLiteral)
        {
            SetMetadata(metadataName, treatMetadataValueAsLiteral ? EscapingUtilities.Escape(metadataValue) : metadataValue);
        }

        private void VerifyForMetadataSet(string metadataName, string metadataValue)
        {
            ErrorUtilities.VerifyThrowArgument(!FileUtilities.IsDerivableItemSpecModifier(metadataName),
                "Shared.CannotChangeItemSpecModifiers", metadataName);

            ErrorUtilities.VerifyThrowArgumentLength(metadataName, nameof(metadataName));
            ErrorUtilities.VerifyThrowArgumentNull(metadataValue, nameof(metadataValue));

            // Make sure the metadata doesn't use any special characters in the name.
            XmlUtilities.VerifyThrowValidElementName(metadataName);

            ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[metadataName] == null,
                "CannotModifyReservedItemMetadata", metadataName);

            ErrorUtilities.VerifyThrow((this.unevaluatedCustomMetadata != null) && (this.evaluatedCustomMetadata != null),
                "Item not initialized properly.");
        }

        /// <summary>
        /// Updates only the metadata tables, not the backing XML. Keep a backup so that the changes
        /// can be reverted.
        /// </summary>
        internal void SetVirtualMetadata(string metadataName, string metadataValue)
        {
            VerifyForMetadataSet(metadataName, metadataValue);

            if (IsPartOfProjectManifest)
            {
                BackupPersistedMetadata();
            }

            unevaluatedCustomMetadata[metadataName] = metadataValue;
            evaluatedCustomMetadata[metadataName] = metadataValue;
        }

        private void BackupPersistedMetadata()
        {
            if (!IsBackedUp)
            {
                unevaluatedCustomMetadataBackup = (CopyOnWriteHashtable)unevaluatedCustomMetadata.Clone();
                evaluatedCustomMetadataBackup = (CopyOnWriteHashtable)evaluatedCustomMetadata.Clone();
            }
        }

        /// <summary>
        /// If the metadata tables were backed up, revert them to the originals and
        /// throw out the backups.
        /// </summary>
        internal void RevertToPersistedMetadata()
        {
            if (IsBackedUp)
            {
                unevaluatedCustomMetadata = unevaluatedCustomMetadataBackup;
                evaluatedCustomMetadata = evaluatedCustomMetadataBackup;
                unevaluatedCustomMetadataBackup = null;
                evaluatedCustomMetadataBackup = null;
            }
        }
        
        /// <summary>
        /// Sets an arbitrary metadata on the item element. These are metadata that the project author has placed on the item
        /// element that have no meaning to MSBuild. They are just arbitrary metadata that travel around with the BuildItem.
        /// Does not backup metadata before making changes.
        /// </summary>
        public void SetMetadata(string metadataName, string metadataValue)
        {
            MustNotBeImported();
            VerifyForMetadataSet(metadataName, metadataValue);

            // this will let us make sure the project item has actually changed before marking it dirty
            bool dirtyItem = false;

            if (IsPartOfProjectManifest)
            {
                SplitChildItemIfNecessary();

                dirtyItem = xml.SetChildValue(metadataName, metadataValue);
            }

            // update the custom metadata cache
            this.unevaluatedCustomMetadata[metadataName] = metadataValue;
            this.evaluatedCustomMetadata[metadataName] = metadataValue;

            if (dirtyItem && IsPartOfProjectManifest)
            {
                // mark the item as dirty, one of the pieces of metadata (or the overall list) has changed;
                // this will mark the parent project as dirty
                MarkItemAsDirty();
            }
        }

        /// <summary>
        /// Removes the specified metadata on the item.
        /// </summary>
        /// <remarks>Removal of well-known metadata is not allowed.</remarks>
        public void RemoveMetadata(string metadataName)
        {
            ErrorUtilities.VerifyThrowArgument(!FileUtilities.IsItemSpecModifier(metadataName), "Shared.CannotChangeItemSpecModifiers", metadataName);
            MustNotBeImported();
            ErrorUtilities.VerifyThrow((this.unevaluatedCustomMetadata != null) && (this.evaluatedCustomMetadata != null), "Item not initialized properly.");

            if (IsBackedByXml)
            {
                SplitChildItemIfNecessary();

                xml.RemoveChildrenByName(metadataName);
            }

            // update the custom attribute cache
            this.unevaluatedCustomMetadata.Remove(metadataName);
            this.evaluatedCustomMetadata.Remove(metadataName);

            MarkItemAsDirty();
        }

        /// <summary>
        /// Returns any default value that this item knows about for the named metadata.
        /// If none is known, returns null.
        /// </summary>
        private string GetDefaultMetadataValue(string metadataName)
        {
            string metadataValue = null;

            if (name != null && itemDefinitionLibrary != null)
            {
                metadataValue = itemDefinitionLibrary.GetDefaultMetadataValue(name, metadataName);
            }

            return metadataValue;
        }

        /// <summary>
        /// Performs path manipulations on the (evaluated/final) item-spec as directed.
        /// </summary>
        /// <param name="modifier">The modifier to apply to the item-spec, such as RootDir or Filename.</param>
        /// <returns>The modified item-spec.</returns>
        /// <exception cref="InvalidOperationException">Thrown when the item-spec is not a path.</exception>
        private string GetItemSpecModifier(string modifier)
        {
            string modifiedItemSpec = 
                FileUtilities.GetItemSpecModifier(Project.PerThreadProjectDirectory, FinalItemSpecEscaped, modifier, ref itemSpecModifiers);

            if (modifiedItemSpec.Length == 0)
            {
                if (String.Equals(modifier, FileUtilities.ItemSpecModifiers.RecursiveDir, StringComparison.OrdinalIgnoreCase))
                {
                    modifiedItemSpec = ExtractRecursivePortionOfFinalItemSpecDirectory();

                    ErrorUtilities.VerifyThrow(itemSpecModifiers != null,
                        "The FileUtilities.GetItemSpecModifier() method should have created the cache for the \"{0}\" modifier.",
                        FileUtilities.ItemSpecModifiers.RecursiveDir);

                    itemSpecModifiers[modifier] = modifiedItemSpec;
                }
            }

            return modifiedItemSpec;
        }

        /// <summary>
        /// Determines if the new item spec that the user is trying to add to the project
        /// already matches an existing wildcarded item declared in the project.  We only
        /// consider it a "match" in very specific circumstances... if there's anything
        /// weird or not-mainline about the new item spec or the existing item, we fail
        /// the match in order to "play it safe".
        /// </summary>
        internal bool NewItemSpecMatchesExistingWildcard(string newItemSpec)
        {
            Project parentProject = GetParentProject();
            ErrorUtilities.VerifyThrow(parentProject != null, "This method should only get called on persisted items.");
            BuildPropertyGroup evaluatedProperties = parentProject.evaluatedProperties;

            if (
                    // The original item spec should have had at least one "*" or "?" in it.
                    FileMatcher.HasWildcards(this.Include) &&

                    // The original item should not have a Condition.
                    (this.Condition.Length == 0) &&

                    // The original item should not have an Exclude.
                    (this.Exclude.Length == 0) &&

                    // The new item spec should NOT have any wildcards.
                    !FileMatcher.HasWildcards(newItemSpec)
                )
            {
                Expander propertyExpander = new Expander(evaluatedProperties);
                string newItemSpecExpandedEscaped = propertyExpander.ExpandAllIntoStringLeaveEscaped(newItemSpec, null);
                // New item spec should not have any unescaped semicolons ... this can really mess us up.
                if (-1 == newItemSpecExpandedEscaped.IndexOf(';'))
                {
                    // Expand any properties in the new item spec that the user gave us.
                    string newItemSpecExpandedUnescaped = EscapingUtilities.UnescapeAll(newItemSpecExpandedEscaped);

                    // Loop over each piece separated by semicolons.
                    List<string> itemIncludePieces = propertyExpander.ExpandAllIntoStringListLeaveEscaped(this.Include, this.IncludeAttribute);
                    foreach (string itemIncludePiece in itemIncludePieces)
                    {
                        bool containsEscapedWildcards = EscapingUtilities.ContainsEscapedWildcards(itemIncludePiece);
                        bool containsRealWildcards = FileMatcher.HasWildcards(itemIncludePiece);

                        // If this is the piece that has the wildcards ...
                        if (containsRealWildcards && !containsEscapedWildcards)
                        {
                            string itemIncludePieceContainingWildcardUnescaped = EscapingUtilities.UnescapeAll(itemIncludePiece);

                            FileMatcher.Result match = FileMatcher.FileMatch(itemIncludePieceContainingWildcardUnescaped, newItemSpecExpandedUnescaped);

                            if (match.isLegalFileSpec && match.isMatch)
                            {
                                // The wildcard in the original item spec will match the new item that
                                // user is trying to add.
                                return true;
                            }
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Gets the parent project of this item, if there is one.
        /// </summary>
        private Project GetParentProject()
        {
            BuildItemGroup parentItemGroup;

            // If this is a child item, then it's really the parent persisted item (the thing that's
            // actually in the project file) that has a pointer to the parent BuildItemGroup.
            if (this.ParentPersistedItem != null)
            {
                parentItemGroup = this.ParentPersistedItem.ParentPersistedItemGroup;
                ErrorUtilities.VerifyThrow(parentItemGroup != null, "Inconsistency -- is this item persisted or not?");
            }
            else
            {
                parentItemGroup = this.ParentPersistedItemGroup;
            }

            if (parentItemGroup != null)
            {
                ErrorUtilities.VerifyThrowNoAssert(parentItemGroup.ParentProject != null, "Persisted BuildItemGroup doesn't have parent project.");
                return parentItemGroup.ParentProject;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Marks the parent project as dirty.
        /// </summary>
        private void MarkItemAsDirty()
        {
            Project parentProject = GetParentProject();
            parentProject?.MarkProjectAsDirty();
        }

        /// <summary>
        /// Marks the parent project as dirty for re-evaluation only.  This means that the actual contents
        /// of the project file XML haven't changed.
        /// </summary>
        private void MarkItemAsDirtyForReevaluation()
        {
            Project parentProject = GetParentProject();
            parentProject?.MarkProjectAsDirtyForReevaluation();
        }

        /// <summary>
        /// Verifies this is not an imported item.
        /// </summary>
        private void MustNotBeImported()
        {
            ErrorUtilities.VerifyThrowInvalidOperation(!importedFromAnotherProject, "CannotModifyImportedProjects");
        }

        /// <summary>
        /// Verifies that an item definition library is supplied.
        /// This is for internal error checking only.
        /// </summary>
        private void MustHaveItemDefinitionLibrary(ItemDefinitionLibrary library)
        {
            ErrorUtilities.VerifyThrow(library != null, "Item definition library required when item is owned by project");
        }

        /// <summary>
        /// Sometimes an item in the project file is declared such that it actually evaluates
        /// to multiple items (we call these "child" items).  Examples include the use of
        /// wildcards, as well as semicolon-separated lists of item specs.  When one of the
        /// child items is modified through the object model, in order to reflect that
        /// change in the project file, we have to split up the original item tag into
        /// multiple item tags.  This method, when called on one of the parent items,
        /// accomplishes exactly that.
        /// </summary>
        private void SplitItem()
        {
            if (!IsPartOfProjectManifest)
            {
                // There's no point splitting an item if it's not an item outside of a target;
                // a modification to such an item is thrown away at the end of the build, so we'll
                // never persist it
                return;
            }

            ErrorUtilities.VerifyThrow(ParentPersistedItemGroup != null, "No parent BuildItemGroup for item to be removed.");

            if (this.ChildItems.Count > 1)
            {
                foreach (BuildItem childItem in this.ChildItems)
                {
                    ErrorUtilities.VerifyThrow(childItem.IsBackedByXml, "Must be backed by XML");

                    childItem.ParentPersistedItem = null;

                    BuildItem newItem = ParentPersistedItemGroup.AddNewItem(childItem.Name, childItem.FinalItemSpecEscaped);

                    if (childItem.Condition.Length > 0)
                    {
                        newItem.Condition = childItem.Condition;
                    }

                    // Copy all the metadata from the original item tag.
                    foreach (XmlNode customMetadata in childItem.ItemElement)
                    {
                        newItem.ItemElement.AppendChild(customMetadata.Clone());
                    }

                    childItem.InitializeFromItemElement(newItem.ItemElement);

                    childItem.ParentPersistedItem = newItem;

                    newItem.ChildItems.AddItem(childItem);
                }

                ParentPersistedItemGroup.RemoveItem(this);
            }
        }

        /// <summary>
        /// Sometimes an item in the project file is declared such that it actually evaluates
        /// to multiple items (we call these "child" items).  Examples include the use of
        /// wildcards, as well as semicolon-separated lists of item specs.  When one of the
        /// child items is modified through the object model, in order to reflect that
        /// change in the project file, we have to split up the original item tag into
        /// multiple item tags.  This method, when called on one of the child items,
        /// accomplishes exactly that.
        /// </summary>
        /// <owner>rgoel</owner>
        internal void SplitChildItemIfNecessary()
        {
            this.ParentPersistedItem?.SplitItem();
        }

        /// <summary>
        /// This creates a shallow clone of the BuildItem.  If this is an xml-backed item,
        /// then the clone references the same XML element as the original, meaning
        /// that modifications to the clone will affect the original.
        /// </summary>
        /// <returns></returns>
        /// <owner>rgoel</owner>
        public BuildItem Clone()
        {
            BuildItem clonedItem;

            if (IsBackedByXml)
            {
                clonedItem = new BuildItem(xml.Element, this.importedFromAnotherProject, this.itemDefinitionLibrary);
                clonedItem.SetEvaluatedItemSpecEscaped(this.evaluatedItemSpecEscaped);
                clonedItem.SetFinalItemSpecEscaped(this.FinalItemSpecEscaped);
                clonedItem.itemSpecModifiers = this.itemSpecModifiers;
                clonedItem.recursivePortionOfFinalItemSpecDirectory = this.recursivePortionOfFinalItemSpecDirectory;
                clonedItem.evaluatedCustomMetadata = this.evaluatedCustomMetadata;
                clonedItem.unevaluatedCustomMetadata = this.unevaluatedCustomMetadata;
                clonedItem.isPartOfProjectManifest = this.isPartOfProjectManifest;
                clonedItem.itemDefinitionLibrary = this.itemDefinitionLibrary;
            }
            else
            {
                clonedItem = VirtualClone();
            }

            // Do not set the ParentPersistedItemGroup on the cloned item, because it isn't really
            // part of the item group.

            return clonedItem;
        }

        /// <summary>
        /// Updates the build item xml backing store with the passed in xml backing store. 
        /// </summary>
        internal void UpdateBackingXml(BuildItemGroupChildXml backingXml)
        {
            xml =  backingXml;
            this.name = xml.Name;
        }

        internal BuildItem VirtualClone()
        {
            return VirtualClone(false /* do not remove references */);
        }

        /// <summary>
        /// Clones the item to a virtual item i.e. an item with no backing XML.
        /// If removeReferences is specified, removes all references which hold on to Projects (or other heavyweight objects)
        /// in order to minimize the transitive size of the clone.
        /// </summary>
        /// <remarks>
        /// This method differs from Clone() in that it always produces a virtual item, even when given an item with backing XML.
        /// Decoupling an item from its XML allows modifications to the clone without affecting the original item.
        /// </remarks>
        internal BuildItem VirtualClone(bool removeReferences)
        {
            ItemDefinitionLibrary definitionLibraryToClone = this.itemDefinitionLibrary;

            if (removeReferences)
            {
                definitionLibraryToClone = null;
            }

           BuildItem virtualClone = 
               new BuildItem
               (
                   null /* this is a virtual item with no backing XML */,
                   name, Include,
                   false, /* PERF NOTE: don't waste time creating a new custom metadata
                           * cache, because we're going to clone the current item's cache */
                   definitionLibraryToClone
               );

            virtualClone.SetEvaluatedItemSpecEscaped(evaluatedItemSpecEscaped);
            virtualClone.SetFinalItemSpecEscaped(FinalItemSpecEscaped);

            // NOTE: itemSpecModifiers don't need to be deep-cloned because they are tied to the finalItemSpec -- when the
            // finalItemSpec changes, they will get reset
            virtualClone.itemSpecModifiers = itemSpecModifiers;
            virtualClone.recursivePortionOfFinalItemSpecDirectory = recursivePortionOfFinalItemSpecDirectory;

            ErrorUtilities.VerifyThrow(unevaluatedCustomMetadata != null && evaluatedCustomMetadata != null, "Item is not initialized properly.");
             
            if (removeReferences)
            {
                // The ItemDefinition is going to be cleared to remove a link between a project instance and the Item when it is in the cache of targetOutputs.
                // We need to merge the ItemDefinition metadata onto the builditemto preserve the metadata on the buildItem when the ItemDefinition
                // is cleared and the item is placed in the cache.
                virtualClone.unevaluatedCustomMetadata = (CopyOnWriteHashtable)GetAllCustomUnevaluatedMetadata();
                virtualClone.evaluatedCustomMetadata = (CopyOnWriteHashtable)GetAllCustomEvaluatedMetadata();
            }
            else
            {
                // Cloning is cheap for CopyOnWriteHashtable so just always do it.
                virtualClone.unevaluatedCustomMetadata = (CopyOnWriteHashtable)this.unevaluatedCustomMetadata.Clone();
                virtualClone.evaluatedCustomMetadata = (CopyOnWriteHashtable)this.evaluatedCustomMetadata.Clone();
           }
   
            return virtualClone;
        }

        /// <summary>
        /// Convert the given array of BuildItems into ITaskItems (BuildItem names are lost)
        /// </summary>
        /// <param name="originalItems"></param>
        /// <returns></returns>
        internal static ITaskItem[] ConvertBuildItemArrayToTaskItems(BuildItem[] originalItems)
        {
            if (originalItems == null)
            {
                return null;
            }

            ITaskItem[] convertedItems = new TaskItem[originalItems.Length];

            for (int i = 0; i < originalItems.Length; i++)
            {
                if (originalItems[i].IsUninitializedItem)
                {
                    convertedItems[i] = new TaskItem(originalItems[i].FinalItemSpecEscaped);
                    foreach (DictionaryEntry metadata in originalItems[i].GetAllCustomEvaluatedMetadata())
                    {
                        convertedItems[i].SetMetadata((string)metadata.Key, EscapingUtilities.UnescapeAll((string)metadata.Value));
                    }
                }
                else
                {
                    convertedItems[i] = new TaskItem(originalItems[i]);
                }
            }

            return convertedItems;
        }

        /// <summary>
        /// Convert the given array of ITaskItems into nameless BuildItems
        /// </summary>
        /// <param name="originalItems"></param>
        /// <returns></returns>
        internal static BuildItem[] ConvertTaskItemArrayToBuildItems(ITaskItem[] originalItems)
        {
            if (originalItems == null)
            {
                return null;
            }

            BuildItem[] convertedItems = new BuildItem[originalItems.Length];

            for (int i = 0; i < originalItems.Length; i++)
            {
                convertedItems[i] = new BuildItem(null, originalItems[i]);
            }

            return convertedItems;
        }

        #endregion

    }
}
